import java.awt.*;
import java.util.*;
import java.math.*;
import javax.swing.*;

/**
 * Handles most game logic
 * 
 * @author  James Wilkinson <jhwilkin@purdue.edu>
 * @version 1.7
 * @since   2011-11-20
 **/
public class GameLogic extends Thread
{
  
    private GameSounds myGameSounds;
    private Centipede myCentipedes[];
    private Mushroom myMushrooms[][];
    private GameCanvas myGameCanvas;
    private Ship myShip;
    private HighScores myHighScores;
    public Frame myFrame;
    private Point myProjectiles[];
  
    //Timing
    private final int BASEFRAMETIME = 5;
    private int centUpdateTimer =0;
    private int superLaserShotTimer =0;
  
    //Many flags for much the same thing lol
    public boolean syncReset;
    public boolean gameOverFlag;
    public boolean paused;
    public boolean gameIsDone;
    public boolean won = false;
    
    //State
    public int level;
    public int difficulty;
    public int score;
    public String thisPerson = "null";
    public int thisScore = 0;
    
    private Random generator;
    
    /**
     * @param inGameSounds  Called to play sound effects
     * @param inGameCanvas  Controls display of game objects
     * @param inHighScores  Called to add to high scores
     * @param inMushrooms   Array of mushrooms
     * @param inCentipedes  Array of centipedes
     * @param inShip        Player's ship
     * @param inProjectiles Array of projectiles
     **/
    public GameLogic(GameSounds inGameSounds, GameCanvas inGameCanvas, HighScores inHighScores, Mushroom inMushrooms[][], Centipede inCentipedes[], Ship inShip, Point inProjectiles[])
    {
        
        myGameCanvas = inGameCanvas;
        
        //Initialize the following with the passed parameters:
        //myGameSounds, myHighScores, myMushrooms, myCentipedes
        //myProjectiles, myShip
        myGameSounds = inGameSounds;
        myHighScores = inHighScores;
        myMushrooms = inMushrooms;
        myCentipedes = inCentipedes;
        myProjectiles = inProjectiles;
        myShip = inShip;
        
        //Create a random generator object named generator
        generator = new Random();
        
        //Initialize syncReset to false
        syncReset = false;
        gameIsDone = false;
        
        //Initialize the difficulty to Settings.startDifficulty
        difficulty = Settings.startDifficulty;
    }
    
    /**
     * Starts the GameLogic thread
     **/
    public void run()
    { 
      
      // This sets up double buffering to smooth out the display
      myGameCanvas.c.createBufferStrategy(2);
        
      // The game logic will loop as long as the program is running
      while(!gameIsDone){
        //This should update the positions and status of Centipedes, projectiles,
        //mushrooms and the ship.  The loop should allow for synchronous resetting
        //triggered through a Boolean.  It should also not process any game logic
        //while the game is paused.  Game over and level up conditions will be
        //determined in this loop.
        
        //Sleep for Base Frame Time, Update timers
        try{
          Thread.sleep(BASEFRAMETIME);
          centUpdateTimer += BASEFRAMETIME;
          superLaserShotTimer += BASEFRAMETIME;
        }catch(Exception e){}
          
        //Reset
        if(syncReset){
          initGame();
          syncReset = false;
        }
        
        //Play if not Paused
        if(!paused){
          
          //Fire SuperLaser
          if(Settings.superLaser){
           if(superLaserShotTimer >= Settings.superLaserDelay && myShip.firing){
              superLaserShotTimer = 0;
              myShip.fire();
            }
          }
          
          //Move
          if(centUpdateTimer >= Settings.centDelay){
            centUpdateTimer = 0;
            moveCentipedes();
          }
          moveProjectiles();
          moveShip();
            
          /*//Game Over
          if(myShip.lives == -1 && myShip.invulnerableTime == 0){
            gameOverFlag = true;
            
            //Get name
            thisPerson = JOptionPane.showInputDialog(null, "Score: " + score + "\nEnter your name:", "Game Over", 1);
            thisScore = score;
            
            if(thisPerson != null){
              myHighScores.addHighScore(thisPerson, thisScore);
              myHighScores.showHighScores(thisPerson, thisScore);
            }
            
            myShip.lives--;
          }*/
        }
          
        //Update the canvas
        myGameCanvas.drawFrame();
      }
      
      myHighScores.writeScores();
    }
    
    /**
     * Initialize a new game
     **/
    public void initGame()
    {       
      //Initialize the difficulty, level and lives to match the corresponding
      //values in Settings.
      
      //If this is a completely new game
      if(!won){
        
        if(myFrame.easyDifficulty.isSelected()){
          Settings.centDelay = Settings.centDelayEasy;
        }else if(myFrame.mediumDifficulty.isSelected()){
          Settings.centDelay = Settings.centDelayMedium;
        }else if(myFrame.hardDifficulty.isSelected()){
          Settings.centDelay = Settings.centDelayHard;
        }
          
        level = Settings.startLevel;
        myShip.lives = Settings.startLives;
        score = 0;
        myGameSounds.newGame();
        
      //If this is a new level
      }else{
        myGameSounds.nextLevel();
      }
        
      //Initialize myShip.invulnerableTime and score to 0.
      myShip.invulnerableTime = 0;
        
      //Remove any leftover centipedes
      for(int i = 0; i < myCentipedes.length; i++){
        myCentipedes[i] = null;
      }
      
      //Remove any leftover projectiles
      for(int i = 0; i < myProjectiles.length; i++){
        myProjectiles[i] = null;
      }
        
      //Create a single centipede of Settings.centipedeStartSize segments heading
      //right and down.  Be sure to initialize all the segments of the Centipede.
      myCentipedes[0] = new Centipede(Settings.centipedeStartSize, Settings.RIGHT, Settings.DOWN);
                
      //Create array of mushrooms and randomly place them on the game grid.
      //Make sure that mushrooms do not overlap and keep them off of the 
      //bottom row.
      for(int x = 0; x < Settings.width; x++){
        for(int y = 0; y < Settings.height; y++){
          myMushrooms[x][y] = null;
        }
      }
      
      for(int i = 0; i < Settings.startShrooms; i++){
        Point p;
        
        do{
          p = new Point(generator.nextInt(Settings.width - 2) + 1,generator.nextInt(Settings.height - 2) + 1);
        }while(overlap(p) == Settings.SHROOM);
        
        myMushrooms[p.x][p.y] = new Mushroom(p, Settings.shroomStartHealth);
      }
      
      //Reset Timers
      centUpdateTimer =0;
      superLaserShotTimer =0;
                
      //Set the gameOverFlag to false
      won = false;
      gameOverFlag = false;
    }
    
    /**
     * Returns sum corresponding to overlapped objects
     * 
     * @param loc Unscaled grid coordinate
     * @return Each potentially overlapping object has a value that is a power of two
     * These values are summed up and then returned to the calling method.
     * The calling method can then run a modulus to find exactly what was impacted.
     * Refer to Settings.java for the return values.
     **/
    private int overlap(Point loc)
    {
      
        int result = Settings.NONE;
        
        //Check to see if the point has impacted or crossed beyond a wall.
        if(loc.x < 0 || loc.x >= Settings.width || loc.y < 0 || loc.y >= Settings.height){
          return Settings.WALL;
        }
        
        //Check all segments of all centipedes to see if they contain the Point loc.
        for(int i = 0; i < myCentipedes.length; i++)
        {
          if(myCentipedes[i] != null && myCentipedes[i].contains(loc) != -1){
            return Settings.CENT;
          }
        }
                
        //Check all mushrooms to see if they contain the Point loc.
        if(myMushrooms[loc.x][loc.y] != null){
          return Settings.SHROOM;
        }
        
        //Check to see if the ship occupies the Point loc.
        if(myShip.loc.x == loc.x && myShip.loc.y == loc.y){
          return Settings.SHIP;
        }
        
        //Check all projectiles to see if they contain the Point loc.
        for(int i = 0; i < myProjectiles.length; i++){
          if(myProjectiles[i] != null && myProjectiles[i].x == loc.x && myProjectiles[i].y == loc.y){
            return Settings.PROJECTILE;
          }
        }
        
        return result;
    }
    
    /**
     * Moves the centipedes to follow the segment ahead of it
     * 
     * Centipedes will try to move its head to the position directly in front of it.
     * It will then have all following segments change its position to that of the segment
     * that was ahead of it.  If the head cannot move to its desired position, then it will
     * move vertically and reverse its horizontal direction.  If at the top/bottom of the screen
     * then reverse the vertical direction as well.  A centipede can share space with a mushroom
     * but that mimics the behavior of the original game and is intended.
     **/
    private void moveCentipedes()
    {
      //Loop through all centipedes and have them move according to the
      //project specifications.
      
      Point lastLoc = null;
      
      for(int i = 0; i < myCentipedes.length; i++){
        if(myCentipedes[i] != null){
          
          for(int x = 0; x < myCentipedes[i].segments.length; x++){
            if(myCentipedes[i].segments[x] != null){
              
              Point tempLoc = new Point(myCentipedes[i].segments[x].x,myCentipedes[i].segments[x].y);
            
              if(x == 0){
              
                Point tryLoc = new Point(myCentipedes[i].segments[x].x,myCentipedes[i].segments[x].y);
                tryLoc.x += myCentipedes[i].horizontal;
              
                int collision = overlap(tryLoc);
                if(collision == Settings.SHROOM ||  collision == Settings.WALL || collision == Settings.CENT){
                  
                  tryLoc.x -= myCentipedes[i].horizontal;
                  tryLoc.y += myCentipedes[i].vertical;
                  if(overlap(tryLoc) == Settings.CENT){
                    break;
                  }
                
                  //redirect
                  myCentipedes[i].horizontal *= -1;
                
                  if(tryLoc.y < 0 || tryLoc.y >= 30){
                    
                    myCentipedes[i].vertical *= -1;
                    tryLoc.y += myCentipedes[i].vertical * 2;
                  }
                }
              
                myCentipedes[i].segments[x] = tryLoc;
              
              }else{
                myCentipedes[i].segments[x] = lastLoc;
              }
          
              lastLoc = tempLoc;
            }
          }
        }
      }
    }
    
    /**
     * Tries to move the ship according to its tryLoc provided by the GameCanvas
     * 
     * If the ship tries to move into a mushroom, its location will be set to a distance away
     * from the mushroom equal to the game's scaling factor (Settings.scale).  A ship can slip
     * between two diagonal mushrooms because of this.  If the ship tries to move into a centipede
     * then it will lose a life, blow up and become invulnerable for Settings.invulnerableTime ms.
     * If the ship loses its last life, then the game is over.
     **/
    private void moveShip()
    {
      
      //Collision with mushroom
      myShip.loc = myShip.tryLoc;
      
      Point gridLoc = new Point((myShip.loc.x + (Settings.scale/2)) / Settings.scale, (myShip.loc.y + (Settings.scale/2)) / Settings.scale);
     
      //traverse nearby grids
      for(int x = -1; x <=1; x++){
        for(int y = -1; y <=1; y++){
          Point cell = new Point(gridLoc.x + x, gridLoc.y + y);
          
          if(overlap(cell) == Settings.SHROOM){
            
            //Get centers
            Point cellCenter = new Point(cell.x * Settings.scale + Settings.scale/2, cell.y * Settings.scale + Settings.scale/2);
            Point shipCenter = new Point(myShip.loc.x + Settings.scale / 2, myShip.loc.y + Settings.scale / 2);
            
            //Get distance
            Point d = new Point(shipCenter.x - cellCenter.x, shipCenter.y - cellCenter.y);
            Double distance = Math.abs(Math.sqrt(d.x*d.x + d.y*d.y));
            
            if(distance < Settings.scale){
              
              //Get angle and handle divide by zero
              double angle = Math.PI/2;
              try{
                angle = Math.atan(-d.y/d.x);
              }catch(Exception e){
                if(shipCenter.y > cellCenter.y){
                  angle = -Math.PI/2;
                }
              }
              
              //Flip horizontal
              int side = 1;
              if(shipCenter.x < cellCenter.x){
                side = -1;
              }
              
              //Update location
              myShip.loc = new Point(((int)(Settings.scale * side * Math.cos(angle)) + cellCenter.x) - Settings.scale/2, ((int)(Settings.scale * -1 * side * Math.sin(angle)) + cellCenter.y) - Settings.scale/2);
            }
          }
        }
      }
      
      //Update invulnerableTime
      if(myShip.invulnerableTime > 0){
        myShip.invulnerableTime -= BASEFRAMETIME;
        
        if(myShip.invulnerableTime < 0){
          myShip.invulnerableTime = 0;
        }
      }
      
      //Check for collision with cent after moving ship.
      if(overlap(new Point((myShip.loc.x+Settings.scale/2) / Settings.scale, (myShip.loc.y+Settings.scale/2) / Settings.scale)) == Settings.CENT && myShip.invulnerableTime == 0 && myShip.lives >= 0){
        myGameSounds.shipExplode();
        myShip.lives--;
        myShip.invulnerableTime = Settings.invulnerableTime;
        
        if(myShip.lives < 0){
          myShip.invulnerableTime = Settings.invulnerableTime * 2;
          myGameCanvas.drawFrame();
          gameOver();
        }
        
        return;
      }
    }
    
    /**
     * Moves the projectiles and checks for collision
     * 
     * Tries to move the projectiles vertically by one pixel.  If the destination overlaps something else,
     * then remove the projectile and react appropriately to the impacted object.
     **/
    private void moveProjectiles()
    {
      
      //Create a loop to move all projectiles.
      for(int i = 0; i < myProjectiles.length; i++){
        if(myProjectiles[i] != null){
          
          //Move
          myProjectiles[i].y -= BASEFRAMETIME;
          Point tempLoc = new Point(myProjectiles[i].x / Settings.scale, myProjectiles[i].y / Settings.scale);
        
          //Delete
          if(myProjectiles[i].y < 0){ 
            myProjectiles[i] = null;
            
          }else{
            //If the projectile impacted a Centipede, increase the score proportionately
            //to the difficulty and whether the user is using a superLaser or not.
            //The impacted Centipede segment will need to be removed and depending on
            //where the Centipede was impacted, it may need to split into two Centipedes.
            //Play the centDie sound.
            //Place a new Mushroom at the location where the Centipede was hit.
            for(int x = 0; x < myCentipedes.length; x++){
              if(myCentipedes[x] != null){
                
                int index = myCentipedes[x].contains(tempLoc);
                if(index >= 0){
                  myGameSounds.centDie();
                  
                  //Update points base on laser and speed
                  int points = 1000;
                  points /= Settings.centDelay;
                  if(Settings.superLaser){
                    points /= 2;
                  }
                  score += points;
                  
                  //Get index of tail
                  int lastSeg = myCentipedes[x].segments.length-1;
                  for(int z = 0; z < myCentipedes[x].segments.length; z++){
                    if(myCentipedes[x].segments[z] == null){
                      lastSeg = z-1;
                      break;
                    }
                  }
                  
                  //Delete Cent
                  if(lastSeg == 0){
                    myCentipedes[x] = null;
                    
                    //Check if won
                    won = true;
                    for(int z = 0; z < myCentipedes.length; z++){
                      if(myCentipedes[z] != null){
                        won = false;
                        break;
                      }
                    }
                    
                    //If won
                    if(won){
                      level++;
                      syncReset = true;
                      
                      //Speed up cent
                      Settings.centDelay -= (int) (Settings.centDelay * 0.1f);
                      if(Settings.centDelay < 0){
                        Settings.centDelay = 0;
                      }
                    }
                    
                  //Delete head
                  }else if(index == 0){
                    
                    for(int z = 0; z < lastSeg; z++){
                      myCentipedes[x].segments[z] = new Point(myCentipedes[x].segments[z+1].x,myCentipedes[x].segments[z+1].y);
                    }
                    myCentipedes[x].segments[lastSeg] = null;
                    
                  //Delete Tail
                  }else if(index == lastSeg){
                    myCentipedes[x].segments[lastSeg] = null;
                    
                  //Split
                  }else{
                    for(int z = 0; z < myCentipedes.length; z++){
                      if(myCentipedes[z] == null){
                        
                        //Create new with length and heading
                        myCentipedes[z] = new Centipede(lastSeg - index, myCentipedes[x].horizontal, myCentipedes[x].vertical);
                        
                        //Shift to new
                        for(int e = 0; e < myCentipedes[z].segments.length; e++){
                          myCentipedes[z].segments[e] = myCentipedes[x].segments[index + 1 + e];
                          myCentipedes[x].segments[index + 1 + e] = null;
                        }
                        
                        myCentipedes[x].segments[index] = null;
                        
                        break;
                      }
                    }
                  }
            
                  //Add Shroom
                  myMushrooms[tempLoc.x][tempLoc.y] = new Mushroom(tempLoc, Settings.shroomStartHealth);
                
                  //Delete Projectile
                  myProjectiles[i] = null;
                }
              }
            }
      
            //If the projectile impacted a Mushroom, decrement the Mushroom's health.
            //If the Mushroom's health is 0, remove the Mushroom.
            //Play the shroomHit sound.
            //Mushroom
            if(myProjectiles[i] != null && myProjectiles[i].y >= 0){
              if(overlap(tempLoc) == Settings.SHROOM){
                
                myGameSounds.shroomHit();
                myMushrooms[tempLoc.x][tempLoc.y].health--;
            
                if(myMushrooms[tempLoc.x][tempLoc.y].health == 0){
                  myMushrooms[tempLoc.x][tempLoc.y] = null;
                }
                
                myProjectiles[i] = null;
              }
            }
          }
        }
      }
    }
    
    /**
     * Flips gameOverFlag to true, plays the game over sound and tries to add a high score
     **/
    public void gameOver()
    {
      
      //Set gameOverFlag to true, stop the ship from firing, play the gameOver sound
      //and try adding the score to the high scores. 
      gameOverFlag = true;
      myShip.firing = false;
      myGameSounds.gameOver();
      
      //Add High Score
      thisPerson = JOptionPane.showInputDialog(null, "Score: " + score + "\nEnter your name:", "Game Over", 1);
      thisScore = score;
            
      if(thisPerson != null){
        myHighScores.addHighScore(thisPerson, thisScore);
        myHighScores.showHighScores(thisPerson, thisScore);
      }
    }
}